let path = require('path')
let fs = require('fs')
let babylon = require('babylon')
let t = require('@babel/types')
let traverse = require('@babel/traverse').default
let generator = require('@babel/generator').default
let ejs = require('ejs')
let { SyncHook } = require('tapable')
// babylon 主要就是把 源码 转换成 AST
// @babel/traverse 
// @babel/types
// @babel/generator
class Compiler{
    // entry output
    constructor(config) {
        this.config = config
        // 需要保存入口文件的路径
        this.entryId // './src/index.js'
        // 需要保存所有的模块依赖
        this.modules = {}
        this.entry = config.entry // 入口路径
        this.root = process.cwd() // 工作路径
        this.hooks = {
            entryOption: new SyncHook(),
            compile: new SyncHook(),
            afterCompile: new SyncHook(),
            afterPlugins: new SyncHook(),
            run: new SyncHook(),
            emit: new SyncHook(),
            done: new SyncHook()
        }
        // 如果传递啦 plugins 参数
        let plugins = this.config.plugins
        if(Array.isArray(plugins)) {
            plugins.forEach(plugin => {
                plugin.apply(this)
            })
        }
        this.hooks.afterPlugins.call()
    }
    run() {
        this.hooks.run.call()
        // 执行 并且创建模块的依赖关系
        this.buildModule(path.resolve(this.root, this.entry), true)
        this.hooks.afterCompile.call()
        // console.log(this.modules, this.entryId)
        // 发射一个文件 打包后的文件
        this.emitFile()
        this.hooks.emit.call()
        this.hooks.done.call()
    }
    getSource(modulePath) { // ./index.less
        let rules = this.config.module.rules
        let content = fs.readFileSync(modulePath, 'utf8')
        // 拿到每个规则来处理
        for (let i = 0; i < rules.length; i++) {
            const rule = rules[i];
            let { test, use } = rule
            let len = use.length - 1
            if(test.test(modulePath)) { // 这个模块需要 对应的 loader 来处理
                function normalLoader() {
                    let loader = require(use[len --])
                    // 获取对应的 loader 函数
                    // 递归调用 loader 实现转化功能
                    content = loader(content) 
                    if(len >= 0) {
                        normalLoader()
                    }
                }
                normalLoader()
            }
        }
        return content
    }
    // 解析源码
    parse(source, parentPath) { // AST 解析语法树
        let AST = babylon.parse(source)
        let dependencies = []
        traverse(AST, {
            CallExpression(p) { // a() require()
                let node = p.node // 对应的节点
                if(node.callee.name === 'require') {
                    node.callee.name = '__webpack_require__'
                    let moduleName = node.arguments[0].value // 取到的就是模块引用的名字
                    moduleName = moduleName + (path.extname(moduleName) ? '': '.js') // a.js
                    moduleName = './' + path.join(parentPath, moduleName) // src/a.js
                    dependencies.push(moduleName)
                    node.arguments = [t.stringLiteral(moduleName)]
                }
            }
        })
        let sourceCode = generator(AST).code
        return {
            sourceCode,
            dependencies
        }
    }
    // 构建模块
    buildModule(modulePath, isEntry) {
        // 拿到模块的内容
        let source = this.getSource(modulePath)
        // 拿到模块 id  modulePath = modulePath - this.root   src/index.js
        let moduleName = './' + path.relative(this.root, modulePath)
        if(isEntry) {
            this.entryId = moduleName // 保存入口的名字
        }
        // 解析需要把 source 源码进行改造 返回一个依赖列表
        let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName)) // ./src
        // console.log(sourceCode, dependencies)
        // 把相对路径和模块中的内容 对应起来
        this.modules[moduleName] = sourceCode
        dependencies.forEach(dep => this.buildModule(path.join(this.root, dep), false)) // 根模块的 附属品 依赖项
    }
    emitFile() { // 发射文件
        // 用数据 渲染我们的 
        // 拿到输出到哪个目录下 输出路径
        let main = path.join(this.config.output.path, this.config.output.filename)
        let templateStr = this.getSource(path.join(__dirname, 'main.ejs')) // 模板的路径
        let code = ejs.render(templateStr, {
            entryId: this.entryId,
            modules: this.modules
        })
        this.assets = {}
        // 资源中 路径对应的代码
        this.assets[main] = code
        console.log(main)
        fs.writeFileSync(main, this.assets[main])
    }
}

module.exports = Compiler